<?php
/**
 * Created by PhpStorm.
 * User: mark
 * Date: 2021/11/29
 * Time: 2:44 下午
 */

namespace app\aliyun\controller;

use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use AlibabaCloud\Rtc\Rtc;
use think\Cache;
use think\Env;
use think\Log;

/**
 * @url https://next.api.aliyun.com/api/rtc/2018-01-11/StartMPUTask?params={}&sdkStyle=dara
 * Class AliyunLive
 * @package app\aliyun\controller
 */
class AliyunRtc
{
    protected $config = [];

    protected $expireTime = 86400;//控制台的访问控制 > URL鉴权生成,有效时间为1天

    /**
     * 初始化客户端
     * AliyunLive constructor.
     * @throws ClientException
     */
    public function __construct()
    {
        $this->config = config('rtc');
        AlibabaCloud::accessKeyClient($this->config['ak_id'], $this->config['ak_secret'])->regionId($this->config['region'])->asDefaultClient();
    }

    /**
     * @url https://github.com/aliyunvideo/AliRtcAppServer/blob/master/php/app/v1/login.php
     * 生成 客户端调用RTC_SDK加入房间的Token信息
     * @param $userId
     * @param $liveId
     * @param string $role
     * @return array
     */
    public function getRtcAuth($userId, $liveId, $role = 'sub'): array
    {
        try
        {
            //生成随机的 $channelId
            $channelId = $liveId;

            // Warning: nonce support the AppKey generated token.
            // the Nonce should be prefix with 'AK-' otherwise the joining verification will failed.
            // eg. nonce: "AK-0464002093ce3dd010cb05356c8b1d0f".
            $nonce = 'AK-' . uniqid('', true);

            // Warning: timestamp is the token expiration time.
            // User can custom defined the expire time of token.
            // eg, Expires in two days. timestamp: 1559890860.
            $timestamp = time() + $this->expireTime;

            $token = $this->createToken($this->config['app_id'], $this->config['app_key'], $channelId, $userId, $nonce, $timestamp);

            return [
                'app_id' => $this->config['app_id'],
                'user_id' => (string)$userId,
                'nonce' => $nonce,
                'channel_id' => (string)$channelId,
                'token' => $token,
                'role' => $role,
                'timestamp' => $timestamp,
                'gslb' => ["https://rgslb.rtc.aliyuncs.com"]
            ];
        }
        catch (\Exception $e)
        {
            Log::write("生成Rtc Auth，失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}", 'error', true);
            return [];
        }
    }

    /**
     * 开始旁路直播
     * @url https://help.aliyun.com/document_detail/109589.html 概述
     * @url https://help.aliyun.com/document_detail/146789.html 当最后一个人离开频道后30秒自动停止任务，您也可以调用StopMPUTask主动结束任务。
     * @url https://help.aliyun.com/document_detail/93183.html 参数意义
     * @param string $channelId 房间id
     * @param string $streamUrl 推流地址
     * @return array
     */
    public function startMPUTask(string $channelId, string $streamUrl): array
    {

        try
        {
            $taskId = (string)time();


            $request = Rtc::v20180111()->startMPUTask();
            $result = $request
                //下面为必传参数
                ->withAppId($this->config['app_id'])//应用ID，仅支持传单个ID。您可以在控制台创建和查询。
                ->withChannelId($channelId)//频道ID，仅支持传单个ID。
                ->withTaskId($taskId)//任务ID，仅支持传单个ID。由大小写字母、数字、下划线、短划线（-）组成，最大55字节。（说明 此ID为旁路转推的标识，需保证唯一。）
                ->withStreamURL($streamUrl)//播推流地址，仅支持传单个地址。生成规则请参见推流地址和播放地址。说明:对已开防盗链鉴权的域名，需要在推流地址中包含鉴权串。禁止同一个StreamURL在不同任务中同时使用。任务停止10S之内，禁止使用同一个StreamURL。
                //下面为非必传参数
                // ->withSubSpecUsers([1])
                ->withMixMode(1)//混流模式。取值：0：单路转推，不混流转码，仅转推原始单路流，无需配置混流转码参数。1：混流转码（默认值），支持混流转码输出。（说明单路转推模式下有效参数：StreamType、SourceType。混流转码模式下有效参数：MediaEncode、LayoutIds、BackgroundColor、SubSpecUsers、CropMode、UserPanes、Backgrounds、Watermarks。）
//                ->withSourceType('camera')//单路转推模式下用户视频输入流。取值：camera：摄像头。shareScreen：屏幕共享。
//                ->withStreamType(0)//单路转推模式下转推音频流。取值：0：转推原始流。1：仅转推音频流。2：仅转推视频流。默认转推原始流。
//                ->withCropMode(1)//视频的裁剪方式。取值：1：保持比例裁剪。2：保持比例留边（默认值）。
//                ->withMediaEncode(31)//编码选项，请参见下文中的MediaEncode枚举值。 MediaEncode枚举值如下所示（地址：https://help.aliyun.com/document_detail/93183.html）
//                ->withBackgroundColor(255)//背景色RGB，默认是0（黑色）。计算公式为R + G × 256 + B × 65536，R（红）、G（绿）、B（蓝）的取值：0~255。 todo 待测试
//                ->withPayloadType(0)//载荷类型。取值：0：不使用载荷。1：使用VideoSEI。 todo 不知道是啥
//                ->withReportVad(0)//语音激励标志。取值：0：不传递语音激励信息。1：传递语音激励信息。
//                ->withRtpExtInfo(0)//RTP扩展头信息。取值：0：不传递。1：传递。
                ->withMediaEncode(31)
                ->withLayoutIds([1])
                ->request();

            Log::write("开启旁路直播，成功，taskId=" . $taskId, 'log', true);

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray(),
                    'task_id' => $taskId
                ]
            ];

            //todo 待调研下面，使用（https://next.api.aliyun.com/api/rtc/2018-01-11/UpdateMPUTask?params={}）这个地址测试：
            //添加语音激励
            //添加人像分割，仅在开启增强参数（人像分割功能）后有效。
            //添加背景图片
            //添加水印
            //添加文字

        }
        catch (ClientException $e)
        {
            $msg = "开始旁路直播，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "开始旁路直播，ServerException失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()};Error Msg:{$e->getErrorMessage()};Error Code:{$e->getErrorCode()}";
        }

        if (!empty($msg))
        {
            Log::write($msg, 'error', true);
        }

        return ['code' => 0, 'msg' => $msg];
    }

    /**
     * 关闭旁路直播
     * @url https://help.aliyun.com/document_detail/93184.html
     * @param $taskId
     * @return array
     */
    public function stopMPUTask($taskId): array
    {

        try
        {
            $result = Rtc::v20180111()->stopMPUTask()->withAppId($this->config['app_id'])->withTaskId($taskId)->request();

            Log::write("关闭旁路直播，成功，taskId=" . $taskId, 'log', true);

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray(),
                    'task_id' => $taskId
                ]
            ];
        }
        catch (ClientException $e)
        {
            $msg = "关闭 旁路直播，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "关闭 旁路直播，ServerException失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()};Error Msg:{$e->getErrorMessage()};Error Code:{$e->getErrorCode()}";
        }

        if (!empty($msg))
        {
            Log::write($msg, 'error', true);
        }

        return ['code' => 0, 'msg' => $msg];
    }

    /**
     * 连麦房间频道
     * @url https://help.aliyun.com/document_detail/93184.html
     * @param $channelId
     * @return array
     */
    public function deleteRtcChannel($channelId): array
    {
        try
        {
            $request = Rtc::v20180111()->deleteChannel();
            $result = $request
                ->withAppId($this->config['app_id'])
                ->withChannelId($channelId)
                ->debug(false) // Enable the debug will output detailed information
                ->connectTimeout(1) // Throw an exception when Connection timeout
                ->timeout(1) // Throw an exception when timeout
                ->request();

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray(),
                ]
            ];
        }
        catch (ClientException $e)
        {
            $msg = "关闭 连麦房间频道，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "关闭 连麦房间频道，ServerException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }

        return ['code' => 0, 'msg' => $msg];
    }

    /**
     * 调用RemoveTerminals将指定终端用户从频道踢出。
     * 参考：https://help.aliyun.com/document_detail/118409.htm?spm=a2c4g.11186623.0.0.46f041fe8q4b3m#t217839.html
     * @param string $channelId
     * @param array $terminalIds
     * @return array
     */
    public function removeTerminals(string $channelId, array $terminalIds): array
    {
        try
        {

            $request = Rtc::v20180111()->removeTerminals();
            $result = $request
                //下面为必传参数
                ->withAppId($this->config['app_id'])//应用ID，仅支持传单个ID。您可以在控制台创建和查询。
                ->withChannelId($channelId)//频道ID，仅支持传单个ID。
                ->withTerminalIds($terminalIds)
                ->request();

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray()
                ]
            ];

        }
        catch (ClientException $e)
        {
            $msg = "调用RemoveTerminals将指定终端用户从频道踢出，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "调用RemoveTerminals将指定终端用户从频道踢出，ServerException失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()};Error Msg:{$e->getErrorMessage()};Error Code:{$e->getErrorCode()}";
        }

        if (!empty($msg))
        {
            Log::write($msg, 'error', true);
        }

        return ['code' => 0, 'msg' => $msg];
    }


    /**
     * 调用CreateEventSubscribe创建订阅房间消息的回调。
     * 参考：https://help.aliyun.com/document_detail/274447.html
     * rtc请求错误码：https://error-center.aliyun.com/status/product/rtc?spm=5176.10421674.search.5.59965919Z70NM6
     * @param string $channelId
     * @param string $clientToken
     * @return array
     */
    public function createEventSubscribe(string $channelId, string $clientToken, string $callBackUrlKey = 'subscribe_call_back_url'): array
    {
        try
        {

            $query = [
                'AppId' => $this->config['app_id'],//订阅的应用ID。
                'CallbackUrl' => $this->config[$callBackUrlKey],//回调地址。回调内容请参见以下回调内容示例。
                'ClientToken' => $clientToken,//客户端创建订阅的幂等标识。
                'ChannelId' => $channelId,
                'NeedCallbackAuth' => true,//是否开启回调鉴权，取值：（true：开启。fasle（默认值）：关闭。详细信息请参见以下回调鉴权说明。）
            ];

            $events[] = 'UserEvent';//监听用户事件
            $events[] = 'ChannelEvent';//监听渠道账号
            foreach ($events as $i => $v)
            {
                $query['Events.' . ($i + 1)] = $v;
            }

            $result = AlibabaCloud::rpc()
                ->product('rtc')
                ->scheme('https') // https | http
                ->version('2018-01-11')
                ->action('CreateEventSubscribe')
                ->method('POST')
                ->host('rtc.aliyuncs.com')
                ->options([
                    'query' => $query,
                ])
                ->request();

            //保存订阅id，等直播结束以后，移除
            $SubscribeId = $result->toArray()['SubscribeId'];
            Cache::set('rtc_subscribe_id_in_' . $channelId, $SubscribeId);

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray()
                ]
            ];

        }
        catch (ClientException $e)
        {
            $msg = "调用CreateEventSubscribe创建订阅房间消息的回调，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "调用CreateEventSubscribe创建订阅房间消息的回调，ServerException失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()};Error Msg:{$e->getErrorMessage()};Error Code:{$e->getErrorCode()}";
        }

        if (!empty($msg))
        {
            Log::write($msg, 'error', true);
        }

        return ['code' => 0, 'msg' => $msg];
    }


    /**
     * 调用 DeleteEventSubscribe 移除订阅房间消息的回调。
     * 参考：https://help.aliyun.com/document_detail/274447.html
     * rtc请求错误码：https://error-center.aliyun.com/status/product/rtc?spm=5176.10421674.search.5.59965919Z70NM6
     * @param string $channelId 频道id
     * @param string $subscribeId 订阅id（如果不为空，那么先采用这个）
     * @return array
     */
    public function removeEventSubscribe(string $channelId, string $subscribeId = ''): array
    {
        try
        {

            $SubscribeId = Cache::get('rtc_subscribe_id_in_' . $channelId);
            if ($subscribeId)
            {//如果传入$subscribeId的话，就使用
                $SubscribeId = $subscribeId;
            }

            $query = [
                'AppId' => $this->config['app_id'],
                'SubscribeId' => $SubscribeId
            ];

            $result = AlibabaCloud::rpc()
                ->product('rtc')
                ->scheme('https') // https | http
                ->version('2018-01-11')
                ->action('DeleteEventSubscribe')
                ->method('POST')
                ->host('rtc.aliyuncs.com')
                ->options([
                    'query' => $query,
                ])
                ->request();

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray()
                ]
            ];

        }
        catch (ClientException $e)
        {
            $msg = "调用CreateEventSubscribe移除订阅房间消息的回调，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "调用CreateEventSubscribe移除订阅房间消息的回调，ServerException失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()};Error Msg:{$e->getErrorMessage()};Error Code:{$e->getErrorCode()}";
        }

        if (!empty($msg))
        {
            Log::write($msg, 'error', true);
        }

        return ['code' => 0, 'msg' => $msg];
    }

    /**
     * 创建token
     * @param $app_id
     * @param $app_key
     * @param $channel_id
     * @param $user_id
     * @param $nonce
     * @param $timestamp
     * @return false|string
     */
    private function createToken($app_id, $app_key, $channel_id, $user_id, $nonce, $timestamp)
    {
        $s = $app_id . $app_key . $channel_id . $user_id . $nonce . $timestamp;
        return hash('sha256', $s);
    }

    /**
     * 删除 订阅房间消息回调  并  删除连麦房间
     */
    public function rmEventSubAndDelRtcChannel(string $channelId)
    {
        // 删除连麦房间回调
        $this->removeEventSubscribe($channelId);
        // 删除 RTC 连麦房间
        $this->deleteRtcChannel($channelId);
        // 停止 录制
        $this->RtcStopRecordTask($channelId);
    }

    /**
     * 开始 视频录制任务
     * @param string $channelId
     * @param String $taskId
     * @return array
     */
    public function RtcStartRecordTask(string $channelId, string $taskId)
    {
        try
        {
            $modeId = Env::get('host.name') === 'online' ? 'nkc0BCXF' : 'oV20nJIK';

            $request = Rtc::v20180111()->startRecordTask();
            $result = $request
                ->withAppId($this->config['app_id'])
                ->withChannelId($channelId)//频道ID。仅支持传单个ID。
                ->withTaskId($taskId)// 任务ID。仅支持传单个ID，需保证唯一。由1~55位大小写字母、数字、下划线、短划线（-）组成。
                ->withTemplateId($modeId)    // 配置模板ID。
//                ->withStreamType(1) // 单流录制模式，取值：   0（默认值）：录制原始流。   1：仅录制音频流。
//                ->debug(true) // Enable the debug will output detailed information
                ->request();

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray(),
                ]
            ];
        }
        catch (ClientException $e)
        {
            $msg = "关闭 连麦房间频道，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "关闭 连麦房间频道，ServerException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }

        return ['code' => 0, 'msg' => $msg];
    }

    /**
     * 停止 视频录制任务
     * @param string $taskId
     * @return array
     */
    public function RtcStopRecordTask(string $taskId)
    {
        try
        {
            $request = Rtc::v20180111()->stopRecordTask();
            $result = $request
                ->withAppId($this->config['app_id'])
                ->withTaskId($taskId)// 任务ID。仅支持传单个ID，需保证唯一。由1~55位大小写字母、数字、下划线、短划线（-）组成。
//                ->debug(true) // Enable the debug will output detailed information
                ->request();

            return [
                'code' => 200,
                'msg' => '请求成功!',
                'data' => [
                    'result' => $result->toArray(),
                ]
            ];
        }
        catch (ClientException $e)
        {
            $msg = "关闭 连麦房间频道，ClientException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }
        catch (ServerException $e)
        {
            $msg = "关闭 连麦房间频道，ServerException 失败：{$e->getMessage()};File:{$e->getFile()};Line:{$e->getLine()}";
        }

        return ['code' => 0, 'msg' => $msg];
    }
}
